/**
 * Anderground project - Subway maps processor application.
 * Copyright (C) 2008-2009 Eldar Abusalimov, Ilya Shurigyn
 * 
 * This file is part of Anderground.
 * 
 * Anderground is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * Anderground is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Anderground.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.anderground.modules.storage.xml;

import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_ANGLE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_CLOSE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_COLOR;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_COUNTRY_CODE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_DX;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_DY;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_FROM;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_ID;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LAST_MODIFIED;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LATITUDE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LINE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_LONGITUDE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_OPEN;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_SMOOTH;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_TIME;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_TO;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_VERSION;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_VISIBLE;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_X;
import static org.anderground.modules.storage.xml.XmlModelStorage.ATTR_Y;
import static org.anderground.modules.storage.xml.XmlModelStorage.DATE_PATTERN;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_ALIAS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_AUTHOR;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_AUTHORS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_CITY;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_CONTENT;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_COUNTRY;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_DESCRIPTION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_LINE;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_LINES;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_LOCATION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_NAME;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_NOTE;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_POSITION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_ROOT;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_SCHEDULE;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_STATION;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_STATIONS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TRANSFER;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TRANSFERS;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TUNNEL;
import static org.anderground.modules.storage.xml.XmlModelStorage.TAG_TUNNELS;
import static org.anderground.modules.storage.xml.XmlModelStorage.VERSION_0_6;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.anderground.core.model.City;
import org.anderground.core.model.Country;
import org.anderground.core.model.Description;
import org.anderground.core.model.Line;
import org.anderground.core.model.ModelData;
import org.anderground.core.model.Station;
import org.anderground.core.model.Transfer;
import org.anderground.core.model.Tunnel;
import org.anderground.core.model.Station.Label;
import org.anderground.core.storage.StorageException;
import org.anderground.core.util.ColorUtils;
import org.anderground.core.util.Coord;
import org.anderground.core.util.Point;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * {@link XmlModelWriter} implementation that uses DOM for XML building.
 * 
 * @author Ilya Shurigyn
 * @author Eldar Abusalimov
 */
public class DomModelWriter extends XmlModelWriter {

	public static final String MAP_VERSION = VERSION_0_6;

	/**
	 * Constructs a new {@link DomModelWriter} that uses the specified
	 * {@link File} for the future reading.
	 * 
	 * @param file
	 *            the file to write a data to
	 */
	public DomModelWriter(File file) {
		super(file);
	}

	/**
	 * Constructs a new {@link DomModelWriter} that uses a {@link File} created
	 * by converting the specified path string.
	 * 
	 * @param path
	 *            the path of a file to write a data to
	 */
	public DomModelWriter(String path) {
		super(path);
	}

	/**
	 * Constructs a new {@link DomModelWriter} that uses the specified
	 * {@link OutputStream} for the future writing.
	 * 
	 * @param outputStream
	 *            the stream to write a data to
	 * @param closeAfterRead
	 *            defines whether to close the stream after
	 *            {@link #write(ModelData)} completes
	 */
	public DomModelWriter(OutputStream outputStream, boolean closeAfterRead) {
		super(outputStream, closeAfterRead);
	}

	@Override
	protected void writeToOutput(ModelData data, OutputStream out)
			throws IOException {
		DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory
				.newInstance();
		Document doc;
		try {
			DocumentBuilder documentBuilder = documentBuilderFactory
					.newDocumentBuilder();
			doc = documentBuilder.newDocument();
		} catch (ParserConfigurationException e) {
			throw new StorageException(e);
		}

		Element root = doc.createElement(TAG_ROOT);
		root.setAttribute(ATTR_VERSION, MAP_VERSION);

		appendCity(doc, root, data.getCity());

		appendDescription(doc, root, data.getDescription());

		Map<Station, Integer> stationsIdsMap = appendStations(doc, root, data
				.getStations());
		if (stationsIdsMap == null) {
			throw new StorageException("unable to append the stations element");
		}

		Map<Line, Integer> linesIdsMap = appendLines(doc, root, data.getLines());
		if (linesIdsMap == null) {
			throw new StorageException("unable to insert the lines element");
		}

		appendTransfers(doc, root, data.getTransfers(), stationsIdsMap);

		appendTunnels(doc, root, data.getTunnels(), stationsIdsMap, linesIdsMap);

		doc.appendChild(root);

		try {
			TransformerFactory transformerFactory = TransformerFactory
					.newInstance();
			Transformer transformer = transformerFactory.newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
			DOMSource source = new DOMSource(doc);
			StreamResult result = new StreamResult(out);
			transformer.transform(source, result);
		} catch (TransformerConfigurationException e) {
			throw new StorageException(e);
		} catch (TransformerException e) {
			throw new StorageException(e);
		}
	}

	private static void appendCity(Document doc, Node root, City city) {
		Element cityElement = doc.createElement(TAG_CITY);

		Element nameElement = doc.createElement(TAG_NAME);
		nameElement.setTextContent(city.getName());
		cityElement.appendChild(nameElement);

		for (String alias : city.getAliases()) {
			Element aliasElement = doc.createElement(TAG_ALIAS);
			aliasElement.setTextContent(alias);
			cityElement.appendChild(aliasElement);
		}

		Element countryElement = doc.createElement(TAG_COUNTRY);
		Country country = city.getCountry();
		countryElement.setAttribute(ATTR_COUNTRY_CODE, country.getCode());
		countryElement.setTextContent(country.getName());
		cityElement.appendChild(countryElement);

		Element locationElement = doc.createElement(TAG_LOCATION);
		Coord location = city.getLocation();
		locationElement.setAttribute(ATTR_LATITUDE, Integer.toString(location
				.getX()));
		locationElement.setAttribute(ATTR_LONGITUDE, Integer.toString(location
				.getY()));
		cityElement.appendChild(locationElement);

		root.appendChild(cityElement);
	}

	private static void appendDescription(Document doc, Node root,
			Description description) {
		Element descriptionElement = doc.createElement(TAG_DESCRIPTION);

		Date currentDate = Calendar.getInstance().getTime();
		SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_PATTERN);
		String lastModified = dateFormat.format(currentDate);

		Element contentElement = doc.createElement(TAG_CONTENT);
		contentElement.setAttribute(ATTR_LAST_MODIFIED, lastModified);
		descriptionElement.appendChild(contentElement);

		Element authorsElement = doc.createElement(TAG_AUTHORS);
		descriptionElement.appendChild(authorsElement);
		for (String author : description.getAuthors()) {
			Element authorElement = doc.createElement(TAG_AUTHOR);
			Element authorNameElement = doc.createElement(TAG_NAME);
			authorNameElement.setTextContent(author);
			authorElement.appendChild(authorNameElement);
			authorsElement.appendChild(authorElement);
		}

		Element noteElement = doc.createElement(TAG_NOTE);
		noteElement.setTextContent(description.getComment());
		descriptionElement.appendChild(noteElement);

		root.appendChild(descriptionElement);
	}

	private static Map<Station, Integer> appendStations(Document doc,
			Node root, Set<Station> stations) {
		Element stationsElement = doc.createElement(TAG_STATIONS);
		Map<Station, Integer> stationsIdsMap = new HashMap<Station, Integer>(
				stations.size(), 1);

		int id = 0;
		for (Station station : stations) {
			id++;
			stationsIdsMap.put(station, id);

			Element stationElement = doc.createElement(TAG_STATION);
			stationElement.setAttribute(ATTR_ID, Integer.toString(id));

			Label label = station.getLabel();
			Point offset = label.getOffset();
			Element name = doc.createElement(TAG_NAME);
			name.setAttribute(ATTR_DX, Integer.toString(offset.getX()));
			name.setAttribute(ATTR_DY, Integer.toString(offset.getY()));
			name.setAttribute(ATTR_ANGLE, Integer.toString(label.getAngle()));
			name
					.setAttribute(ATTR_VISIBLE, Boolean.toString(label
							.isVisible()));
			name.setTextContent(station.getName());
			stationElement.appendChild(name);

			Element scheduleElement = doc.createElement(TAG_SCHEDULE);
			scheduleElement.setAttribute(ATTR_OPEN, station.getOpenTime()
					.toString());
			scheduleElement.setAttribute(ATTR_CLOSE, station.getCloseTime()
					.toString());
			stationElement.appendChild(scheduleElement);

			Element positionElement = doc.createElement(TAG_POSITION);
			Coord position = station.getPosition();
			positionElement.setAttribute(ATTR_X, Integer.toString(position
					.getX()));
			positionElement.setAttribute(ATTR_Y, Integer.toString(position
					.getY()));
			stationElement.appendChild(positionElement);

			Element locationElement = doc.createElement(TAG_LOCATION);
			Coord location = station.getLocation();
			locationElement.setAttribute(ATTR_LATITUDE, Integer
					.toString(location.getX()));
			locationElement.setAttribute(ATTR_LONGITUDE, Integer
					.toString(location.getY()));
			stationElement.appendChild(locationElement);

			stationsElement.appendChild(stationElement);
		}

		root.appendChild(stationsElement);
		return stationsIdsMap;
	}

	private static Map<Line, Integer> appendLines(Document doc, Node root,
			Set<Line> lines) {
		Element linesElement = doc.createElement(TAG_LINES);
		Map<Line, Integer> linesIdsMap = new HashMap<Line, Integer>(lines
				.size(), 1);

		int id = 0;
		for (Line line : lines) {
			id++;
			linesIdsMap.put(line, id);

			Element lineElement = doc.createElement(TAG_LINE);
			lineElement.setAttribute(ATTR_ID, Integer.toString(id));
			lineElement.setAttribute(ATTR_COLOR, ColorUtils.format(line.getColor()));
			Element name = doc.createElement(TAG_NAME);
			name.setTextContent(line.getName());
			lineElement.appendChild(name);
			linesElement.appendChild(lineElement);
		}

		root.appendChild(linesElement);
		return linesIdsMap;
	}

	private static void appendTunnels(Document doc, Node root,
			Set<Tunnel> tunnels, Map<Station, Integer> stationsIdsMap,
			Map<Line, Integer> linesIdsMap) {
		Element tunnelsElement = doc.createElement(TAG_TUNNELS);

		for (Tunnel tunnel : tunnels) {
			Integer fromStationId = stationsIdsMap.get(tunnel.getFromStation());
			Integer toStationId = stationsIdsMap.get(tunnel.getToStation());
			if (fromStationId == null || toStationId == null) {
				throw new RuntimeException(
						"abnormal: null retrieved from stationsRowsMap");
			}
			Integer lineId = linesIdsMap.get(tunnel.getLine());
			if (lineId == null) {
				throw new RuntimeException(
						"abnormal: null retrieved from linesRowsMap");
			}

			Element tunnelElement = doc.createElement(TAG_TUNNEL);
			tunnelElement.setAttribute(ATTR_FROM, fromStationId.toString());
			tunnelElement.setAttribute(ATTR_TO, toStationId.toString());
			tunnelElement.setAttribute(ATTR_LINE, lineId.toString());
			tunnelElement.setAttribute(ATTR_TIME, Integer.toString(tunnel
					.getTime()));
			tunnelElement.setAttribute(ATTR_SMOOTH, Boolean.toString(tunnel
					.isSmooth()));
			tunnelsElement.appendChild(tunnelElement);
		}

		root.appendChild(tunnelsElement);
	}

	private static void appendTransfers(Document doc, Node root,
			Set<Transfer> transfers, Map<Station, Integer> stationsIdsMap) {
		Element transfersElement = doc.createElement(TAG_TRANSFERS);

		for (Transfer transfer : transfers) {
			Integer fromStationId = stationsIdsMap.get(transfer
					.getFromStation());
			Integer toStationId = stationsIdsMap.get(transfer.getToStation());
			if (fromStationId == null || toStationId == null) {
				throw new RuntimeException(
						"abnormal: null retrieved from stationsRowsMap");
			}

			Element transferElement = doc.createElement(TAG_TRANSFER);
			transferElement.setAttribute(ATTR_FROM, fromStationId.toString());
			transferElement.setAttribute(ATTR_TO, toStationId.toString());
			transferElement.setAttribute(ATTR_TIME, Integer.toString(transfer
					.getTime()));
			transfersElement.appendChild(transferElement);
		}

		root.appendChild(transfersElement);
	}

}
